OpenID Connect 所定義的術語中,有兩個互相傳遞資訊的角色為 OpenID 服務提供者(簡稱 OP)與依賴方(簡稱 RP)。
Hydra 的設計是把控制 OpenID Connect 流程的任務交由 OpenID Provider 也就是 Hydra 來處理,而登入驗證實作交給 Login Provider 處理,授權實作交給 Consent Provider 處理。依照這個設計,若想要用 Hydra 模擬完整的 OP 與 RP,就需要啟動四個以上的服務才能正常運行。聽起來很複雜,這件事留到未來再慢慢釐清,今天將會使用 Hydra 提供的 Docker Compose 的設定檔以及 Docker Image 來準備模擬環境。
準備 Docker 環境可以參考 30 天與鯨魚先生做好朋友 Day 2 建置 Docker 環境。筆者是使用 Lima 搭配預設的 containerd 建置的,訊息可能會跟 Docker 略有不同,但原則上差異不大。
官方有提供五分鐘快速體驗教學,以下的內容是參考官方教學改寫的。
首先要把 Hydra 原始碼下載回來,並切換成 v1.11.10 版本:
git clone https://github.com/ory/hydra.git
cd hydra && git checkout v1.11.10
原始碼的根目錄有很多 quickstart
檔,這些都是啟動懶人包服務的 Docker Compose 配置設定檔:
❯ ls quickstart*
quickstart-cockroach.yml
quickstart-cors.yml
quickstart-debug.yml
quickstart-hsm.yml
quickstart-jwt.yml
quickstart-mysql.yml
quickstart-postgres.yml
quickstart-prometheus-config.yml =>> 這個是 Prometheus 設定檔先不管它
quickstart-prometheus.yml
quickstart-tracing.yml
quickstart.yml
Hydra 快速體驗的設計方法是,以 quickstart.yml
為基底,再依需求追加額外功能。比方說如果想要 MySQL 的版本,官方提供的指令如下:
docker-compose -f quickstart.yml \
-f quickstart-mysql.yml \
up --build
以這個例子來說明,Docker Compose 會把 quickstart.yml
當作基礎,然後 quickstart-mysql.yml
設定追加或覆蓋上去。這個例子實際的意思指的是使用 MySQL 作為儲存服務,如果想用 PostgreSQL 的話,則是改用 quickstart-postgres.yml
。
containerd 實際啟動 MySQL 或 migratie 可能會有時間差問題,導致 Container 無法正常啟動,下面是單一服務逐步啟動的方法:
# MySQL 啟動約要 20 秒時間
❯ docker-compose -f quickstart.yml -f quickstart-mysql.yml up -d mysqld
# Hydra 資料庫 migrate 大概需要 20 秒
❯ docker-compose -f quickstart.yml -f quickstart-mysql.yml up -d hydra-migrate
# 啟動 Hydra
❯ docker-compose -f quickstart.yml -f quickstart-mysql.yml up -d hydra
# 啟動測試用的 Login 與 Consent 服務
❯ docker-compose -f quickstart.yml -f quickstart-mysql.yml up -d consent
啟動完後,可以使用 docker-compose ps
確認開啟了哪些 server:
$ docker-compose -f quickstart.yml -f quickstart-mysql.yml ps
NAME COMMAND SERVICE STATUS PORTS
hydra_hydra-migrate_1 "hydra migrate -c /e…" hydra-migrate Exited (0) 3 minutes ago
hydra_mysqld_1 "docker-entrypoint.s…" mysqld running 0.0.0.0:3306->3306/tcp
hydra_consent_1 "/bin/sh -c npm run …" consent running 0.0.0.0:3000->3000/tcp
hydra_hydra_1 "hydra serve -c /etc…" hydra running 0.0.0.0:4444->4444/tcp, 0.0.0.0:4445->4445/tcp, 0.0.0.0:5555->5555/tcp
以下使用表格整理出已啟動服務和用途:
Service | Port | 用途 |
---|---|---|
hydra-migrate | 無 | 執行 DB migration,當成功執行完後就會關閉 |
mysqld | 3306 | 資料庫服務 |
consent | 3000 | Hydra 官方提供身分驗證與授權的測試服務 |
hydra | 4444、4445、5555 | Hydra 的主要服務 |
啟動完成後可以使用 curl 指令來確定 Hydra 是否正常啟用:
curl http://127.0.0.1:4444/health/ready
curl http://127.0.0.1:4445/health/ready
其中 4444
是提供給公開使用者或應用程式存取的, 4445
則是內部管理員使用的。官方的 Swagger 文件在將 API 分組時,是把 4444 稱為 Public API, 4445 則是 Admin API,未來會用這兩個術語來稱呼這兩個 port 所開放的 API。
兩個 port 有各自的 health check API,若一切正常則會回應:
{"status":"ok"}
剛建立好服務,資料庫會是空的,因此需要手動註冊一個 RP 才能開始執行登入流程。
可以透過 Admin API 或是透過 Hydra CLI 註冊:
docker-compose -f quickstart.yml \
-f quickstart-mysql.yml \
exec hydra hydra --endpoint http://127.0.0.1:4445/ clients --skip-tls-verify \
create \
--id my-rp \
--secret my-secret \
--grant-types authorization_code,implicit,client_credentials,refresh_token \
--response-types "code,token,id_token,token code,code id_token,id_token token,id_token token code" \
--scope openid \
--token-endpoint-auth-method client_secret_basic \
--callbacks https://oidcdebugger.com/debug
這個指令會使用 container 執行 Hydra CLI,去呼叫 Admin API 並帶入對應的參數來註冊 client。裡面可以看到 Day02 - Day04 所提到的許多關鍵字,以下一一說明。
首先是 id 與 secret,id 指的是 RP 的唯一識別碼,換句話說就是指應用程式的「帳號」,而 secret 指的是這個帳號的「密碼」。
--id my-rp --secret my-secret
Hydra 是允許可以直接在註冊的時候直接輸入密碼,只是它就會提醒你這樣會有被別人偷看 history 的風險。
You should not provide secrets using command line flags, the secret might leak to bash history and similar systems
當然也可以選擇不輸入 secret,Hydra 會透過亂數產生 secret 並印出:
OAuth 2.0 Client ID: my-rp
OAuth 2.0 Client Secret: ECDb639*******************
接著是 Grant Types,指應用程式能夠使用哪些授權類型。這裡是啟用所有類型的範例:
--grant-types authorization_code,implicit,client_credentials,refresh_token
Response Types,指應用程式能夠使用哪些回傳類型。這裡是啟用所有類型,但注意的是,多個類型回傳方法也算是「一種」類型:
--response-types "code,token,id_token,token code,code id_token,id_token token,id_token token code"
Scope 是應用程式能夠請求的授權範圍,這裡對應到了 OpenID Connect 協定提到的 openid
:
--scope openid
Token Endpoint Auth Method 指的是要取得 Token 的時候要用什麼方法驗證「應用程式的身分」,這裡是使用 Basic Auth:
--token-endpoint-auth-method client_secret_basic
Callback 指的是授權完成後,要回到哪個指定的路徑:
--callbacks https://oidcdebugger.com/debug
OpenID Connect debugger 是一個測試用的網址,可以針對 OpenID Connect 的流程做測試,剛好適合用在今天的快速體驗。
一切就緒之後,再來就可以開始走登入流程了,首先打開測試網址,輸入以下資訊:
欄位內容如下:
欄位 | 內容 |
---|---|
Authorize URI (required) | http://127.0.0.1:4444/oauth2/auth |
Redirect URI (required) | https://oidcdebugger.com/debug |
Client ID (required) | my-rp |
Scope (required) | openid |
State | (程式自動產生) |
Nonce | (程式自動產生) |
Response type (required) | 先勾 token 、id_token 這兩個 |
Response mode (required) | fragment |
PKCE 是另一個主題,未來會再另外說明。
填完上面資訊後,網站最下面會根據 OpenID Connect 協定產生授權請求如下:
http://127.0.0.1:4444/oauth2/auth
?client_id=my-rp
&redirect_uri=https://oidcdebugger.com/debug
&scope=openid
&response_type=id_token token code
&response_mode=fragment
&state={程式自動產生}
&nonce={程式自動產生}
這裡有用七個參數,是 OpenID Connect 定義的 Authentication Request 規格,以下列出欄位和說明:
欄位 | 必填 | 說明 |
---|---|---|
client_id |
REQUIRED | RP 的唯一識別碼,是註冊的時候帶入的 ID。 |
redirect_uri |
REQUIRED | 當授權完成後,會將使用者導去哪,這裡的值必須與註冊的 callback 完全一致。 |
scope |
REQUIRED(OpenID Connect) | 授權範圍,使用 openid 表示要發身分識別 token(ID token)。 |
response_type |
REQUIRED | 回傳給 RP 的回應類型。 |
response_mode |
OPTIONAL | 回應模式,指定回傳 RP 的方法。 |
state |
RECOMMENDED | 此為防止 CSRF 攻擊的隨機字串。 |
nonce |
OPTIONAL | 為了避免重送攻擊的一次性亂數字串。 |
按下 SEND REQUEST 後,網址就會轉到 http://127.0.0.1:3000/login,也就是登入頁。這是 Hydra 用來做測試的:
上面有提示測試的使用者帳密為何,輸入 foo@bar.com
與 foobar
並點擊 Log in 之後,會出現第二個頁面是授權頁:
上面的 openid
核選鈕打勾後即代表授權此項目,接著點擊 Allow access 後,接著 Hydra 會帶著授權回應回到 Debugger。這裡可以先關心一下網址,它其實是使用 Fragment 來傳輸授權回應的:
https://oidcdebugger.com/debug#access_token=br1X-KqIP7taR_SJ4MVH-y_5EEIOFUW49vFkAaVtVIM.soHhJsvW4vMdU-XGZGejKQ_YyLvUQ6ISFDRDAicjbBQ&expires_in=3599&id_token=eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpoeWRyYS5vcGVuaWQuaWQtdG9rZW4iLCJ0eXAiOiJKV1QifQ.eyJhY3IiOiIwIiwiYXRfaGFzaCI6IjRZTG85WnQ2a3FfZ2hHMy1tTWtJUWciLCJhdWQiOlsibXktcnAiXSwiYXV0aF90aW1lIjoxNjYzNjg3ODA4LCJleHAiOjE2NjM2OTE0MTEsImlhdCI6MTY2MzY4NzgxMSwiaXNzIjoiaHR0cDovLzEyNy4wLjAuMTo0NDQ0LyIsImp0aSI6ImY0NDBmMmQzLTk1ZWUtNGNlYS04ZmNiLTBhZDc0NDQzODVkMyIsIm5vbmNlIjoiaDB1djBxMDd3eSIsInJhdCI6MTY2MzY4Nzc5OSwic2lkIjoiMDUzYTczYWQtZWE4My00YTQ3LWI1ZjYtNWJkMjc3MjhiYjgyIiwic3ViIjoiZm9vQGJhci5jb20ifQ.XsUk-BTb3IIGqW8khtFRFVGLGlwmCa_wRdyydz6sHB8w6FddfAVHoAuzNRwjEYK5tA0Z-bcqJdxEwlukpUpNf5KnjLBl180nZu0Q0YvRHS_kAyvL3mtnFb4XuFL1BdaIWToa_ht3XCWc64XlSymGXVUb-Ns5yqJK63i8RyUP4hauf3P79jl7NrFExhqXAmbeFW115omT_JNL_s0yTq-X2w84elCdYb-zEtCrRWdjWMrfpmoEQRwr3OxBHWEDD03TggW3Nh_vMlqqsN8PANU8GQlbpngHHtmbkGZptAWog07RxN2ofO8-t9X63_bfqyu9anWRJ0XO4-1UVTxtXuh19XTxg85CyWAk5ymucKHwS_7996S1NUvZwrADXG-Z3WbiKGGikQcgU0WXKwN-eBTk5-OV8ms_OueUGT7hccBn4nA1lNXuJ3C5PchqZQeEg7xO8Zg1CsEawbFJwXyzDbXJcBCxsD4HVrSgsVZ2pBnZ2teJhFTMenbJ-R2iNZMozYPP8nhXx0ACtqlLFq98ukcsyWiYrGvCMI3XH7IByr1YxqrqT_nI5zuJHljqY4JE-vIIKxoGObUPfUtoPr0ZHHJFxt97PVnQ9xphU4VPljknxc-nrdl6N0Xb7dM55qVSbOA8r6GDMjebveHLJJoOntjZr4vT0GwSfyYuY7J3yHuPhUU&scope=openid&state=x5bqkzhxurp&token_type=bearer
授權回應資料解析結果如下:
回到 Debugger 後,Debugger 會把授權回應裡面的內容解析出來,其中有兩個資訊,是 Access token 與 ID token,這兩個剛好對應到回應類型(Response type)的 token
與 id_token
。
Access token 需要呼叫 API 來確認,這個之後的章節再來確認。而 ID token 的內容可以立即解析出來,如下:
Header
{
"alg": "RS256",
"kid": "public:hydra.openid.id-token",
"typ": "JWT"
}
Payload
{
"acr": "0",
"at_hash": "4YLo9Zt6kq_ghG3-mMkIQg",
"aud": [
"my-rp"
],
"auth_time": 1663687808,
"exp": 1663691411,
"iat": 1663687811,
"iss": "http://127.0.0.1:4444/",
"jti": "f440f2d3-95ee-4cea-8fcb-0ad7444385d3",
"nonce": "h0uv0q07wy",
"rat": 1663687799,
"sid": "053a73ad-ea83-4a47-b5f6-5bd27728bb82",
"sub": "foo@bar.com"
}
裡面幾個欄位代表的意義可以參考 OpenID Connect 說明。
欄位 | 說明 |
---|---|
at_hash |
這是從 Access token 做雜湊演算後的結果,是用來確認 ID token 與 Access token 是否為同時發行的 |
aud |
ID token 的受眾,簡單來說就是指 ID token 要給誰用 |
auth_time |
使用者身分驗證發生的時間 |
exp |
過期時間 |
iat |
發行時間 |
iss |
發行單位 |
jti |
JWT 的唯一識別碼 |
sid |
Session ID,用在登出協定上 |
sub |
使用者的唯一識別碼 |
到這邊為止,Access token 與 ID token 應用程式都拿到了,因此授權的流程到此就完成了。再來應用程式把 ID token 保存起來,直到執行登出或過期;而 Access token 則是存取資源所要使用的,未來的章節再來討論。